const fs = require('fs')
const path = require('path')
const assert = require('assert')
const datastore = require('nedb-promise')
const util = require('util')

const OPERATION_TYPE = {
  ADD_PIXEL: 1,
  UPDATE_PIXEL: 2,
  ADD_USER: 3,
  UPDATE_USER: 4,
}

const AUTO_COMPACT_INTERVAL = 3600 * 1000

module.exports = class Repository {
  constructor(options) {
    this.dir = options.dir
    this.logFile = path.join(this.dir, 'logs.db')
    this.pixelFile = path.join(this.dir, 'pixels.db')
    this.userFile = path.join(this.dir, 'users.db')
    this.binLogFile = path.join(this.dir, 'revert-logs.db')

    this.logDB = datastore({ filename: this.logFile, autoload: true })
    this.pixelDB = datastore({ filename: this.pixelFile, autoload: true })
    this.userDB = datastore({ filename: this.userFile, autoload: true })
    this.binLogDB = datastore({ filename: this.binLogFile, autoload: true })

    this.newPixels = new Map
    this.newUsers = {}
  }
  async init() {
    console.log('repository init...')
    await this.logDB.ensureIndex({ fieldName: 'seq', unique: true })
    await this.binLogDB.ensureIndex({ fieldName: 'seq', unique: true })

    await this.pixelDB.ensureIndex({ fieldName: 'coordinate', unique: true })
    await this.pixelDB.ensureIndex({ fieldName: 'user' })

    await this.userDB.ensureIndex({ fieldName: 'address', unique: true })

    this.pixelDB.nedb.persistence.setAutocompactionInterval(AUTO_COMPACT_INTERVAL)
    this.userDB.nedb.persistence.setAutocompactionInterval(AUTO_COMPACT_INTERVAL)
  }
  async commitLog(log) {
    console.log('commit log:', JSON.stringify(log))
    const { user, coordinate, seq } = log

    if (!this.newUsers[user]) {
      this.newUsers[user] = { count: 1 }
    } else {
      this.newUsers[user].count++
    }
    this.newUsers[user].lastPixel = coordinate
    this.newPixels.set(coordinate, log)

    const p = await this.pixelDB.findOne({ coordinate })
    const u = await this.userDB.findOne({ address: user })
    let binLog = { seq, operations: [] }
    if (p) {
      binLog.operations.push({
        coordinate,
        type: OPERATION_TYPE.UPDATE_PIXEL,
        old: p,
      })
    } else {
      binLog.operations.push({
        coordinate,
        type: OPERATION_TYPE.ADD_PIXEL,
      })
    }
    if (u) {
      binLog.operations.push({
        address: user,
        type: OPERATION_TYPE.UPDATE_USER,
        old: u,
      })
    } else {
      binLog.operations.push({
        address: user,
        type: OPERATION_TYPE.ADD_USER,
      })
    }

    await this.logDB.insert(log)
    await this.binLogDB.insert(binLog)
    await this._updatePixel(log)
    await this._updateUser(log)
  }
  async rollbackLog(seq) {
    console.log('rollback log: %d', seq)
    const binLog = await this.binLogDB.findOne({ seq })
    const { coordinate, address } = binLog
    assert(binLog)
    for (const op of binLog.operations) {
      switch (op.type) {
        case OPERATION_TYPE.ADD_PIXEL:
          await this.pixelDB.remove({ coordinate })
          break
        case OPERATION_TYPE.UPDATE_PIXEL:
          await this.pixelDB.update({ coordinate }, op.old)
          break
        case OPERATION_TYPE.ADD_USER:
          await this.userDB.remove({ address })
          break
        case OPERATION_TYPE.UPDATE_USER:
          await this.userDB.update({ address }, op.old)
          break
      }
    }
    await this.binLogDB.remove({ seq })
    await this.logDB.remove({ seq })
  }
  async rollbackLogUntil(seq) {
    console.log('rollback logs until %d', seq)
    this.clearNewPixels()
    this.clearNewUsers()
    const count = await this.binLogDB.count({})
    for (let i = count - 1; i > seq; i--) {
      await this.rollbackLog(i)
    }
    console.log('rollback logs completed')
  }
  async stat() {
    console.log('state manage stat')
    const c1 = await this.binLogDB.count({})
    const c2 = await this.logDB.count({})
    const c3 = await this.pixelDB.count({})
    const c4 = await this.userDB.count({})
    console.log(c1, c2, c3, c4)
  }
  async getLogs(offset, limit) {
    const min = offset
    const max = offset + limit - 1
    const logs = await this.logDB.find({ seq: { $gte: min, $lte: max } })
    return logs
  }
  async getLogCount() {
    const count = await this.logDB.count({})
    return count
  }
  getNewUsers() {
    return this.newUsers
  }
  getNewPixels() {
    return this.newPixels
  }
  clearNewPixels() {
    this.newPixels = new Map
  }
  clearNewUsers() {
    this.newUsers = {}
  }
  getTopUsers(n) {
    return new Promise((resolve, reject) => {
      this.userDB.nedb.find({}).sort({ pixelsPainted: -1 }).limit(n).exec((err, results) => {
        if (err) reject(err)
        else resolve(results)
      })
    })
  }
  getLatestLogs(n) {
    return new Promise((resolve, reject) => {
      this.logDB.nedb.find({}).sort({ seq: -1 }).limit(n).exec((err, results) => {
        if (err) reject(err)
        else resolve(results)
      })
    })
  }
  async getAllPixels() {
    return await this.pixelDB.find({})
  }
  async getOwnedCount(owner) {
    return this.pixelDB.count({ user: owner })
  }
  getOwnedPixels(owner, offset, limit) {
    return new Promise((resolve, reject) => {
      this.pixelDB.nedb
        .find({ user: owner })
        .sort({ seq: -1})
        .skip(offset)
        .limit(limit).exec((err, results) => {
          if (err) reject(err)
          else resolve(results)
        })
    })
  }
  async getPixel(coordinate) {
    return await this.pixelDB.findOne({ coordinate })
  }
  async getUserCount() {
    return await this.userDB.count({})
  }

  async _updatePixel(log) {
    const coordinate = log.coordinate
    const modifier = log
    await this.pixelDB.update({ coordinate }, modifier, { upsert: true })
  }
  async _updateUser(log) {
    const modifier = {
      $inc: {
        spent: log.price,
        pixelsPainted: 1,
      },
      $set: {
        seq: log.seq
      }
    }
    const address = log.user
    await this.userDB.update({ address }, modifier, { upsert: true })
  }
}